package it.softphone.cmc.server.service.impl;

import static it.softphone.commons.util.LogUtil.logger;
import it.softphone.cmc.server.service.EntityManagerBaseService;
import it.softphone.cmc.server.service.QueryTask;
import it.softphone.cmc.server.service.exception.CMCException;
import it.softphone.cmc.server.service.impl.EntityManagerProxy;
import it.softphone.commons.exception.NullParameterException;
import it.softphone.commons.spring.ApplicationConfig;

import java.util.Collections;
import java.util.List;
import java.util.Map;

import javax.persistence.EntityManager;
import javax.persistence.EntityManagerFactory;
import javax.persistence.EntityTransaction;
import javax.persistence.NoResultException;
import javax.persistence.Query;
import javax.persistence.TemporalType;

import org.apache.commons.configuration.Configuration;

import com.google.appengine.api.datastore.KeyFactory;

public class GAEEntityManagerBaseServiceImpl implements EntityManagerBaseService{
	

    protected static final ThreadLocal <java.util.Stack<EntityManager>> transactionContext = new ThreadLocal<java.util.Stack<EntityManager>> () {

		@Override
		protected java.util.Stack<EntityManager> initialValue() {
			return new java.util.Stack<EntityManager>();
		}

    };

	
	protected GAEEntityManagerBaseServiceImpl() {
		super();
	}

	private static final EntityManagerFactory getEntityManagerFactory() {
		return ApplicationConfig.getBean(EntityManagerFactory.class);
	}
	
	public static final Configuration getBaseConfig() { 
		return ApplicationConfig.getBean(Configuration.class, "baseConfig"); 	
	}
		
	/**
	 * 
	 * @param <T>
	 * @param method
	 * @param task
	 * @return
	 * @throws CMCException
	 */
	public final <T> T exec( String method, EMTask<T> task ) throws CMCException {
		
		EntityManager em = (transactionContext.get().isEmpty()) ? null : transactionContext.get().peek(); 
		
		boolean isNew = (em==null);
		
		if( isNew ) {
			EntityManagerFactory emf = getEntityManagerFactory();
			em = emf.createEntityManager();
			transactionContext.get().push(em);			
		}

		try {
			
			return task.exec( (isNew) ? new EntityManagerProxy(em) : em );

		}
		catch( CMCException ex ) {
			throw ex;
		}
		catch( NoResultException ex ) {
			logger.debug( String.format("request [%s] has no result!", method) );
			return null;
		}
		catch( Exception ex ) {
			throw new CMCException( method, ex );
		}
		finally {
			if( isNew ) {
				transactionContext.get().pop().close();
			}
		}
	
	}
	
	/**
	 * 
	 * @param method
	 * @param task
	 * @return
	 * @throws CMCException
	 */
	public final <R> R execTransaction( String method, EMTask<R> task ) throws CMCException{
		
		R result = null;
		
		EntityManager em = (transactionContext.get().isEmpty()) ? null : transactionContext.get().peek(); 
		
		boolean isNew = (em==null);
				
		if( isNew ) {
			EntityManagerFactory emf = getEntityManagerFactory();
			em = emf.createEntityManager();
			transactionContext.get().push(em);			
		}

		final EntityTransaction tx = em.getTransaction();
		
		if( isNew ) tx.begin();
		try {
			
			result = task.exec( (isNew) ? new EntityManagerProxy(em) : em );
			
			if( isNew && tx.isActive() ) tx.commit();

		}
		catch( CMCException ex ) {
			if( isNew && tx.isActive() ) tx.rollback();
			throw ex;
		}
		catch( NoResultException ex ) {
			logger.warn( String.format("request [%s] has no result! this exception will close transaction", method) );
			if( isNew && tx.isActive() ) tx.commit();
			return null;
		}
		catch( Exception ex ) {
			if( isNew && tx.isActive() ) tx.rollback();
			throw new CMCException( method, ex );
		}
		finally {
			if( isNew ) {
				EntityManager emPopped = transactionContext.get().pop();

				emPopped.close();
			}
		}
	
		return result;
	}
	
	/**
	 * 
	 * @param method
	 * @param task
	 * @return
	 * @throws CMCException
	 */
	@Override
	public final <R> R execNewTransaction( String method, EMTask<R> task ) throws CMCException{
		
		R result = null;
		
		EntityManager em = null; 
		
		boolean isNew = (em==null);
				
		if( isNew ) {
			EntityManagerFactory emf = getEntityManagerFactory();
			em = emf.createEntityManager();
			transactionContext.get().push(em);		
			
			logger.debug(String.format( "==> PUSH TX[%s] EM ===> [%s]\n", method, em));
		}

		final EntityTransaction tx = em.getTransaction();
		
		if( isNew ) tx.begin();
		try {
			
			result = task.exec( (isNew) ? new EntityManagerProxy(em) : em );
			
			if( isNew && tx.isActive() ) tx.commit();

		}
		catch( CMCException ex ) {
			if( isNew && tx.isActive() ) tx.rollback();
			throw ex;
		}
		catch( NoResultException ex ) {
			logger.warn( String.format("request [%s] has no result! this exception will close transaction", method) );
			if( isNew && tx.isActive() ) tx.commit();
			return null;
		}
		catch( Exception ex ) {
			if( isNew && tx.isActive() ) tx.rollback();
			throw new CMCException( method, ex );
		}
		finally {
			if( isNew ) {
				EntityManager emPopped = transactionContext.get().pop();
				
				logger.debug(String.format("==> POP TX[%s] EM ===> [%s]\n\tEM USED [%s]\n", method, emPopped, em));

				em.close();
			}
		}
	
		return result;
	}

	/**
	 * 
	 * @param query
	 * @param key
	 * @param value
	 */
	protected void parseQueryParameter( Query query, String key, Object value ) {
		
		char typeCh = key.charAt(0);
		
		if( value instanceof Number ) {
			
			Number n = (Number)value;
			
			if( typeCh=='l' ) {
				query.setParameter( key, n.longValue());
				return;
			}
			if( typeCh=='f' ) {
				query.setParameter( key, n.floatValue());
				return;
			}
			if( typeCh=='g' ) {
				query.setParameter( key, n.doubleValue());
				return;
			}
			if( typeCh=='i' ) {
				query.setParameter( key, n.intValue());
				return;
			}
			if( typeCh=='d' || typeCh=='t' || typeCh=='s' ) {
				value = new java.util.Date(n.longValue());
			}
		}
		else if( value instanceof java.util.Date ) {
			
			if( typeCh=='d' ) {
				query.setParameter( key, (java.util.Date)value, TemporalType.DATE);
				return;
			}
			if( typeCh=='t' ) {
				query.setParameter( key,  (java.util.Date)value, TemporalType.TIME);
				return;
			}

			query.setParameter( key,  (java.util.Date)value, TemporalType.TIMESTAMP);
			return;
		}
		else if( value instanceof String ) {
			
			if( typeCh=='k' ) {
				query.setParameter( key, KeyFactory.stringToKey( value.toString() ) );
				return;
			}
		}

		query.setParameter(key, value);
		
	}
	
	@SuppressWarnings("unchecked")
	protected <T> void find( EntityManager em, String sqlCommand, java.util.Map<String,Object> paramMap, QueryTask<List<T>> task ) throws Exception {
		
		java.util.List<T> result = null;
		
		Query query = em.createQuery( sqlCommand );
		
		for( java.util.Map.Entry<String,Object> e : paramMap.entrySet() ) {
			Object value = e.getValue();
			String key = e.getKey();
			
			parseQueryParameter(query, key, value);
			
		}
		result = query.getResultList();
			
		task.run(result);
			
		
	}
	
	@SuppressWarnings("unchecked")
	protected <T> void find( EntityManager em, String sqlCommand, java.util.List<Object> paramList, QueryTask<List<T>> task ) throws Exception {
		
		java.util.List<T> result = null;
		
		Query query = em.createQuery( sqlCommand );
		
		for( int i=0; i <paramList.size(); ++i ) {
			query.setParameter(i+1, paramList.get(i));
		}
		result = query.getResultList();
			
		task.run(result);
			
		
	}
	
	@SuppressWarnings("unchecked")
	protected <R> R findSingle( EntityManager em, String sqlCommand, java.util.Map<String,Object> paramMap ) {
		
		Object result = null;
		
		Query query = em.createQuery( sqlCommand );
		
		for( java.util.Map.Entry<String,Object> e : paramMap.entrySet() ) {
			Object value = e.getValue();
			String key = e.getKey();
			
			parseQueryParameter(query, key, value);
		}

		try {
			result = query.getSingleResult();
		}
		catch( NoResultException e ) {
			logger.debug( String.format("request [%s] has no result!", sqlCommand) );
			return null;
		}
			
		
		return (R)result;
			
		
	}
	
	@SuppressWarnings("unchecked")
	protected <R> R findSingle( EntityManager em, String sqlCommand, java.util.List<Object> paramList ) {
		
		Object result = null;
		
		Query query = em.createQuery( sqlCommand );
		
		for( int i=0; i <paramList.size(); ++i ) {
			query.setParameter(i+1, paramList.get(i));
		}
		
		try {
			result = query.getSingleResult();
		}
		catch( NoResultException e ) {
			logger.debug( String.format("request [%s] has no result!", sqlCommand) );
			return null;
		}
		
		return (R)result;
	}

	@SuppressWarnings("unchecked")
	@Override
	public <T> void find( final String sqlCommand, final Object paramMap, final QueryTask<List<T>> task ) throws CMCException{
			if( sqlCommand==null ) throw new NullParameterException("sqlCommand");
			if( task==null ) throw new NullParameterException("task");
			
			if( paramMap instanceof java.util.Map ) {
				final java.util.Map<String,Object> pMap = (Map<String, Object>) paramMap;
				exec("find", new EMTask<Void>() {

					@Override
					public Void exec(EntityManager em) throws Exception {

						find(em, sqlCommand, pMap, task);
						return null;
					}
					
				});
				
			}
			else if( paramMap instanceof java.util.List ) {
				final java.util.List<Object> pMap = (List<Object>) paramMap;
				exec("find", new EMTask<Void>() {

					@Override
					public Void exec(EntityManager em) throws Exception {

						find(em, sqlCommand, pMap, task);
						return null;
					}
					
				});
			}
			else if( paramMap instanceof it.softphone.commons.json.server.JSONArray ) {

				final java.util.List<Object> pMap = (List<Object>) ((it.softphone.commons.json.server.JSONArray)paramMap).internalArray();
				exec("find", new EMTask<Void>() {

						@Override
						public Void exec(EntityManager em) throws Exception {
							find(em, sqlCommand, pMap, task);
							return null;
						}
						
					});
						
			}
			else if( paramMap instanceof it.softphone.commons.json.server.JSONObject ) {
						
				final java.util.Map<String,Object> pMap = ((it.softphone.commons.json.server.JSONObject)paramMap).internalMap();
				exec("find", new EMTask<Void>() {

					@Override
					public Void exec(EntityManager em) throws Exception {
						find(em, sqlCommand, pMap, task);
						return null;
					}
					
				});
			}
			else {
				logger.warn( "find: unrecognized parameters format. Ignored!");
				exec("find", new EMTask<Void>() {

					@Override
					public Void exec(EntityManager em)  throws Exception{
						find(em, sqlCommand, Collections.emptyList(), task) ;
						return null;
					}
					
				});
			}
			
			
	}

	@SuppressWarnings("unchecked")
	@Override
	public Object findSingle( final String sqlCommand, final Object paramMap ) throws CMCException{

		if (sqlCommand == null)
			throw new NullParameterException("sqlCommand");

		if (paramMap instanceof java.util.Map) {
			final java.util.Map<String, Object> pMap = (Map<String, Object>) paramMap;
			Object result = exec("findSingle", new EMTask<Object>() {

				@Override
				public Object exec(EntityManager em) {

					Object result = findSingle(em, sqlCommand, pMap);
					return result;
				}

			});

			return result;

		} else if (paramMap instanceof java.util.List) {
			final java.util.List<Object> pMap = (List<Object>) paramMap;
			return exec("findSingle", new EMTask<Object>() {

				@Override
				public Object exec(EntityManager em) {

					return  findSingle(em, sqlCommand, pMap);
				}

			});
		} else if (paramMap instanceof it.softphone.commons.json.server.JSONArray) {

			final java.util.List<Object> pMap = (List<Object>) ((it.softphone.commons.json.server.JSONArray) paramMap)
					.internalArray();
			return exec("findSingle", new EMTask<Object>() {

				@Override
				public Object exec(EntityManager em) {
					return findSingle(em, sqlCommand, pMap);
				}

			});

		} else if (paramMap instanceof it.softphone.commons.json.server.JSONObject) {

			final java.util.Map<String, Object> pMap = ((it.softphone.commons.json.server.JSONObject) paramMap)
					.internalMap();
			return exec("findSingle", new EMTask<Object>() {

				@Override
				public Object exec(EntityManager em) {
					return findSingle(em, sqlCommand, pMap);
				}

			});
		} else {
			logger.warn("findSingle: unrecognized parameters format. Ignored!");
			return exec("findSingle", new EMTask<Object>() {

				@Override
				public Object exec(EntityManager em) {
					return findSingle(em, sqlCommand, Collections.emptyList());
				}

			});
		}

		}
	

}
